package com.cordys.opensource.crom;

import java.lang.reflect.Constructor;
import java.util.Date;

import com.cordys.cpc.bsf.busobject.BSF;
import com.cordys.cpc.bsf.busobject.BusObject;
import com.cordys.cpc.bsf.busobject.BusObjectConfig;
import com.cordys.cpc.bsf.busobject.exception.BsfRuntimeException;
import com.cordys.cpc.bsf.classinfo.RelationInfo_FK;
import com.cordys.opensource.crom.sql.Column;
import com.cordys.opensource.crom.sql.Query;
import com.cordys.opensource.crom.sql.Table;
import com.cordys.opensource.crom.sql.Where.Operator;

/**
 * This class has framework logic for the Revisioning pattern in the database tables.
 * This pattern works as follows:
 * 
 * For a certain domain object, e.g. Solution, we need to keep track of all the changes that
 * have been done to it. 
 * For this purpose, these domain objects have corresponding Revision tables. E.g., Solution table has a SolutionRevision table.
 * The 'changable' properties are stored in the Revision table, the others, like primary key, are stored in the main table.
 * 
 * @see com.cordys.opensource.crom.BusObjectWrapper
 * @see com.cordys.opensource.crom.BusObjectMetadata
 * @see com.cordys.opensource.crom.DomainObject
 * @see com.cordys.opensource.crom.RevisionedObject
 * @see com.cordys.opensource.crom.DomainObjectEventListener
 */
public class RevisionObjectEventListener
{
	private BusObjectWrapper domainObjectWrapper;
	private BusObjectMetadata metadata;
	private BusObject domainObject;
	
	private final Date createdOn = new Date();
	private final String createdBy = BSF.getUser();

	RevisionObjectEventListener(BusObjectWrapper domainObjectWrapper, BusObjectMetadata metadata)
	{
		this.domainObjectWrapper = domainObjectWrapper;
		this.metadata = metadata;
		this.domainObject = domainObjectWrapper.getDomainObject();
	}
	
	/**
	 * Returns the dbRevision object for the domain object. If none is yet created, then a new one is 
	 * created and stored in the registry. The insert method is NOT invoked on the db object.
	 * E.g., for a Solution object, this would return an instance of crom.example.db.SolutionRevision
	 * @param domainObject
	 * @return The db object for the domain object.
	 */
	BusObject getNewRevision()
	{
		BusObject newRevision = domainObjectWrapper.getPersistenceMapping().getNewRevision();
		if (newRevision==null)
		{
			try
			{
				Constructor<?> constructor = metadata.getDbRevisionClass().getConstructor(BusObjectConfig.class);
				newRevision = (BusObject) constructor.newInstance(BusObjectConfig.TRANSIENT);
				domainObjectWrapper.getPersistenceMapping().setNewRevision(newRevision);
			}
			catch (Exception letsCaptureAllExceptionsInOne)
			{
				throw new RuntimeException("Fatal error instantiating the dbClass "+metadata.getDbRevisionClass(), letsCaptureAllExceptionsInOne);
			}
		}
		return newRevision;
	}
	
	/**
	 * Reads the active revision from the database.
	 * @param domainObject
	 * @return
	 */
	private BusObject getActiveRevision()
	{
		BusObject activeRevisionObject = domainObjectWrapper.getPersistenceMapping().getActiveRevision();
		if (activeRevisionObject==null)
		{
			// Create a query that will read the current revision.
			Query query = new Query(metadata.getDbRevisionClass());
			Table revisionTable = query.getTable(metadata.getDbRevisionClass());
	
			// Determine the key fields that link the SolutionRevision to Solution
			//  Typically this will be only a single column, but it can be a key based on multiple columns.
			RelationInfo_FK fk = BusObjectHelper.getForeignKeyRelation(metadata.getDbClass(), metadata.getDbRevisionClass());
			String[] revisionKeyFields = fk.getRelatedAttributes();
			String[] dbFields = fk.getLocalAttributes();
			for (int i=0; i<revisionKeyFields.length; i++)
			{
				// Select the proper key with it's value, e.g. for SolutionRevision it is SolutionId
				Column keyColumn = revisionTable.getColumn(revisionKeyFields[i]);
				String keyValue = domainObject.getStringProperty(dbFields[i]); // This would pick the value for Solution.Id from the domain object
				query.where(keyColumn, Operator.Equals, keyValue);
			}
			// ... and select ExpiredOn is NULL
			Column expirationColumn = revisionTable.getColumn(RevisionedObject.EXPIRED_ON);
			query.where(expirationColumn, Operator.Equals, null);
			activeRevisionObject = query.getObject();

			if (activeRevisionObject==null)
			{
				// TODO: this seems a major mismatch in database?
				// Apparently it is currently not available, or it has already expired?
				//  probably raise an exception, but not sure here
				throw new BsfRuntimeException("There is no active revision for the object so it cannot be updated or deleted. Object: "+domainObject);
			}
			
			domainObjectWrapper.getPersistenceMapping().setActiveRevisionObject(activeRevisionObject);
			
			// ... and start tracking changes
			activeRevisionObject.setAsPersistent();
		}
		return activeRevisionObject;
	}
	
	private void fillCreationFields()
	{
		BusObject newRevision = getNewRevision();
		// Log the user who dunnit
		newRevision.setProperty(RevisionedObject.CREATED_BY, createdBy, 0);
		// Set creation date to now.
		newRevision.setProperty(RevisionedObject.CREATED_ON, createdOn, 0);
	}
	
	private void fillExpiryFields()
	{
		BusObject activeRevision = getActiveRevision();
		// Set creation date to now.
		activeRevision.setProperty(RevisionedObject.EXPIRED_ON, createdOn, 0);
		// And update the object.
		activeRevision.update();
	}

	void onBeforeInsert()
	{
		// Make the dbRevisionObject as "inserted"
		BusObject newRevision = getNewRevision();
		newRevision.insert();
		
		// Now map the attributes from the domain object into the revision object
		BusObjectHelper.mapDBClassAttributes(newRevision, domainObject, false);
		
		// Set the referential integrity with db object
		BusObject dbObject = domainObjectWrapper.getPersistenceMapping().getDatabaseObject();
		BusObjectHelper.relateObjects(newRevision, dbObject, BusObjectHelper.getForeignKeyRelation(newRevision.getClass(), dbObject.getClass()));
		
		fillCreationFields();
	}
	
	void onAfterInsert()
	{
		// Again map attributes to take also include those attributes that have
		//  have been set during the domain object's onInsert method (i.e., the application logic)
		BusObjectHelper.mapDBClassAttributes(getNewRevision(), domainObject, false);
		// Again fill the creation fields (the old versions of those fields are present in domainObject, and have to be overwritten again)
		fillCreationFields();
	}

	void onBeforeUpdate()
	{
		if (domainObject.getOriginalObject()==null || BusObjectHelper.objectsAreEqual(domainObject, domainObject.getOriginalObject()))
		{
			// Domain Object is not changed.
			return;
		}
		
		// Active revision expires now.
		fillExpiryFields();

		// Create the new revision
		BusObject newRevision = getNewRevision();
		// Update the new revision with the actual values of the domain object
		BusObjectHelper.mapDBClassAttributes(newRevision, domainObject, false);
		// Set the referential integrity with db object
		BusObject dbObject = domainObjectWrapper.getPersistenceMapping().getDatabaseObject();
		BusObjectHelper.relateObjects(newRevision, dbObject, BusObjectHelper.getForeignKeyRelation(newRevision.getClass(), dbObject.getClass()));
		// Fill the temporal pattern
		fillCreationFields();
		// Now insert the new object.
		newRevision.insert();
	}

	void onAfterUpdate()
	{
		BusObject newRevision = getNewRevision();
		// Again map attributes to take also include those attributes that have
		//  have been set during the domain object's onUpdate method (i.e., the application logic)
		BusObjectHelper.mapDBClassAttributes(newRevision, domainObject, false);
		// Again fill the creation fields (the old versions of those fields are present in domainObject, and have to be overwritten again)
		fillCreationFields();
	}

	void onBeforeDelete()
	{
		// Active revision expires now.
		fillExpiryFields();
	}

	void onAfterDelete()
	{
		fillExpiryFields();
	}

	void onBeforeCommit()
	{
	}

	void onBeforeAbort()
	{
	}

	void onAfterCommit()
	{
		// make transient to prevent that the update listeners will be invoked again.
		//  making transient can only be done on non-nested objects (i.e., the root level object in the transaction).
		//  Therefore we navigate to the root object that is not nested.
		BusObject object = domainObject;
		while (object.sys_isInnerObject())
		{
			object = object.sys_getOuterObject();
		}
		if (object.isPersistent())
		{
			object.makeTransient();
		}
		
		
		BusObject dbRevisionObject = domainObjectWrapper.getPersistenceMapping().getActiveRevision();
		if (dbRevisionObject==null)
		{
			return;
		}

		// Map all DB properties back into the object (except for the empty and NULL values)
		BusObjectHelper.mapDBClassAttributes(dbRevisionObject, domainObject, true);
	}

	void onAfterAbort()
	{
	}
}